package net.turnbig.wechat.bot;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

import javax.net.ssl.SSLContext;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy;
import org.apache.http.impl.client.DefaultServiceUnavailableRetryStrategy;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.LaxRedirectStrategy;
import org.apache.http.impl.client.StandardHttpRequestRetryHandler;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.ssl.TrustStrategy;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.ThreadFactoryBuilder;

import net.turnbig.wechat.bot.utils.DateUtils;
import net.turnbig.wechat.bot.utils.PatternUtils;

/**
 *
 * @author Woo Cubic
 * @date   2017-05-10 13:53:01
 */
public class WechatBot {

	static final Logger logger = LoggerFactory.getLogger(WechatBot.class);

	static final String USER_AGENT = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) "
			+ "Chrome/57.0.2987.98 Safari/537.36";

	static final String ENCODING = "UTF-8";

	public static final String HTTP_OK = "200";
	public static final String BASE_URL = "https://webpush2.weixin.qq.com/cgi-bin/mmwebwx-bin";
	public static final String JS_LOGIN_URL = "https://login.weixin.qq.com/jslogin";
	public static final String QRCODE_URL = "https://login.weixin.qq.com/qrcode/";

	//@off
	// special system users
	public static final List<String> FILTER_USERS = Arrays.asList("newsapp", "filehelper", "weibo", "qqmail",
			"fmessage", "tmessage", "qmessage", "qqsync", "floatbottle", "lbsapp", "shakeapp", "medianote", "qqfriend",
			"readerapp", "blogapp", "facebookapp", "masssendapp", "meishiapp", "feedsapp", "voip", "blogappweixin",
			"weixin", "brandsessionholder", "weixinreminder", "wxid_novlwrv3lqwv11", "gh_22b87fa7cb3c",
			"officialaccounts", "notification_messages", "wxid_novlwrv3lqwv11", "gh_22b87fa7cb3c", "wxitil",
			"userexperience_alarm", "notification_messages");
	
	public static final String[] HEARTBEAT_SERVERS = { 
		"webpush.wx.qq.com", 
		"webpush.wx2.qq.com", 
		"webpush.weixin.qq.com", 
		"webpush2.weixin.qq.com", 
		"webpush.wechat.com",
		"webpush1.wechat.com", 
		"webpush2.wechat.com", 
		"webpush1.wechatapp.com" 
	};
	//@on

	CloseableHttpClient browser; // simulate browser
	WechatContext context = new WechatContext(); // wechat 用户上下文信息
	String tempFolder;

	private ScheduledExecutorService heartBeatPool;

	public WechatBot() {
		init();
	}

	public void init() {
		initTempFolder();
		initHttpBrowser();
	}

	/**
	 * initial QR-code temp folder
	 */
	public void initTempFolder() {
		try {
			// 用户临时目录
			tempFolder = System.getProperty("java.io.tmpdir") + "wechat-bot";
			File directory = new File(tempFolder);
			FileUtils.forceMkdir(directory);
			// delete temp files
			File[] files = directory.listFiles();
			for (File file : files) {
				if (file.getName().endsWith(".jpg")) {
					long millis = new Date().getTime() - file.lastModified();
					if (millis > 60 * 60 * 1000) {
						FileUtils.deleteQuietly(file);
					}
				}
			}
		} catch (IOException e) {
			logger.error("should not happen", e);
		}
	}

	/**
	 * 
	 */
	public void initHttpBrowser() {
		System.setProperty("https.protocols", "TLSv1");
		System.setProperty("jsse.enableSNIExtension", "false");

		// add connection pool configuration
		HttpClientBuilder httpBuilder = HttpClientBuilder.create();
		httpBuilder.setUserAgent(USER_AGENT);
		httpBuilder.setRedirectStrategy(new LaxRedirectStrategy());
		httpBuilder.setMaxConnPerRoute(20);
		httpBuilder.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy());
		// httpBuilder.setMaxConnTotal(200);

		//@off
		RequestConfig config = RequestConfig.custom()
				.setSocketTimeout(60 * 1000)
				.setConnectTimeout(60 * 1000)
				.setCookieSpec(CookieSpecs.DEFAULT)
				.build();//@on
		httpBuilder.setDefaultRequestConfig(config);

		httpBuilder.setServiceUnavailableRetryStrategy(new DefaultServiceUnavailableRetryStrategy(5, 10));
		httpBuilder.setRetryHandler(new StandardHttpRequestRetryHandler(5, true));

		try {
			// ignore SSL verify
			SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, new TrustStrategy() {
				@Override
				public boolean isTrusted(java.security.cert.X509Certificate[] arg0, String arg1)
						throws java.security.cert.CertificateException {
					return true;
				}
			}).build();
			httpBuilder.setSSLContext(sslContext);
		} catch (Exception e) {
			// ignore, should not happen
		}

		this.browser = httpBuilder.build();
	}

	private String getUrl(String uri) {
		if (uri.startsWith("https://")) {
			return uri;
		}
		return this.context.getBaseURI() + uri;
	}

	private String getContent(HttpUriRequest request) {
		try {
			CloseableHttpResponse execute = browser.execute(request);
			String returned = EntityUtils.toString(execute.getEntity(), ENCODING);
			return returned;
		} catch (IOException e) {
			throw new WechatNetworkException("Network Connection issue", e);
		}
	}

	private String getContent(String url) {
		try {
			HttpUriRequest request = RequestBuilder.get().setUri(getUrl(url)).build();
			CloseableHttpResponse execute = browser.execute(request);
			String returned = EntityUtils.toString(execute.getEntity(), ENCODING);
			return returned;
		} catch (IOException e) {
			throw new WechatNetworkException("Network Connection issue", e);
		}
	}

	/**
	 * @param url
	 * @return 
	 */
	private RequestBuilder buildPostRequest(String url) {
		RequestBuilder rb = RequestBuilder.post().setUri(url);
		rb.addHeader("Content-Type", ContentType.APPLICATION_JSON.toString());
		HashMap<String, Object> body = context.getBaseRequestBody();
		rb.setEntity(new StringEntity(JSON.toJSONString(body), ContentType.APPLICATION_JSON));
		return rb;
	}

	private String getUUID() throws WechatException {
		RequestBuilder rb = RequestBuilder.get().setUri(JS_LOGIN_URL);
		rb.addParameter("appid", "wx782c26e4c19acffb");
		rb.addParameter("fun", "new");
		rb.addParameter("lang", "zh_CN");
		rb.addParameter("_", DateUtils.getEpochSecondsStr());
		rb.addHeader("Content-Type", "application/x-www-form-urlencoded");
		HttpUriRequest request = rb.build();
		String response = this.getContent(request);
		logger.debug("Get UUID response: {}", response);

		// response example:
		// window.QRLogin.code = 200; window.QRLogin.uuid = "QeHpcMBjiA==";
		if (StringUtils.isNotBlank(response)) {
			String code = StringUtils.substringBetween(response, "window.QRLogin.code = ", "; window.QRLogin.uuid");
			if ("200".equals(code)) {
				String uuid = StringUtils.substringBetween(response, "window.QRLogin.uuid = \"", "\";");
				return uuid;
			}
		}
		throw new WechatException("Wechat Server refuse to provide UUID for QR login");
	}

	/**
	 * 获取登录二维码
	 * 
	 * @return QR-code image file path
	 * @throws WechatException 
	 */
	public String getLoginQRCode() throws WechatException {
		String uuid = this.getUUID();
		try {
			logger.info("Use UUID {} to fetch QR code for login", uuid);
			RequestBuilder rb = RequestBuilder.post().setUri(QRCODE_URL + uuid);
			rb.addParameter("t", "webwx");
			rb.addParameter("_", DateUtils.getEpochSecondsStr());

			CloseableHttpResponse execute = browser.execute(rb.build());
			HttpEntity entity = execute.getEntity();

			// save QR code image to TEMP folder
			String filepath = tempFolder + File.separator + new Date().getTime() + ".jpg";
			FileOutputStream ffo = new FileOutputStream(new File(filepath));
			IOUtils.copy(entity.getContent(), ffo);
			IOUtils.closeQuietly(ffo);
			EntityUtils.consumeQuietly(entity);

			logger.info("Wechat login QR code has been saved to: {}", filepath);
			this.context.setUuid(uuid); // update context UUID
			return filepath;
		} catch (IOException e) {
			throw new WechatNetworkException("Network Connection issue", e);
		}
	}

	/**
	 * 	 * <h3>获取当前的登录状态</h3>
	 * 
	 * 注意：
	 * <li>调用该方法前，请先调用  {@link WechatBot#getLoginQRCode()} 以获取登录二维码。</li>
	 * <li>需要循环调用该方法以获取到最终状态，因为用户登录过程需要一定的时间</li>
	 * @param autoLogin					是否自动登录
	 * @return {@link LoginStatus}
	 * @throws WechatException
	 */
	public LoginStatus getLoginStatus(boolean autoLogin) throws WechatException {
		String url = "https://login.weixin.qq.com/cgi-bin/mmwebwx-bin/login";
		RequestBuilder rb = RequestBuilder.get().setUri(url);
		rb.addParameter("tip", "1");
		rb.addParameter("uuid", this.context.getUuid());
		rb.addParameter("_", DateUtils.getEpochSecondsStr());

		String content = this.getContent(rb.build());
		logger.debug("Get login status response: {}", content);

		if (StringUtils.isNotBlank(content)) {
			String code = StringUtils.substringBetween(content, "window.code=", ";");
			if (StringUtils.isNotBlank(code)) {
				LoginStatus status = LoginStatus.from(Integer.parseInt(code));
				if (status != null) {
					logger.info("Current login status is: {}", status);
					if (LoginStatus.Success.equals(status) && autoLogin) {
						String redirectUrl = PatternUtils.getGroup(content, "window.redirect_uri=\"(\\S+?)\";");
						this.login(redirectUrl);
					}
					return status;
				} else {
					// 其他可能存在的未知状态
					logger.error("New login status code {}, please contact admin", code);
				}
			}
		}

		throw new WechatException("获取登录状态失败");
	}

	public void login(String redirectUrl) throws WechatException {
		redirectUrl = redirectUrl + "&fun=new";
		logger.info("Login wechat use url: {}", redirectUrl);

		String content = this.getContent(redirectUrl);
		if (StringUtils.isNotBlank(content)) {
			String baseURI = StringUtils.substringBeforeLast(redirectUrl, "/");
			// init context
			context.setBaseURI(baseURI);
			context.setDeviceId("e" + DateUtils.getEpochSecondsStr());
			context.setUin(PatternUtils.getGroup(content, "<wxuin>(\\S+)</wxuin>"));
			context.setSid(PatternUtils.getGroup(content, "<wxsid>(\\S+)</wxsid>"));
			context.setSkey(PatternUtils.getGroup(content, "<skey>(\\S+)</skey>"));
			context.setPassTicket(PatternUtils.getGroup(content, "<pass_ticket>(\\S+)</pass_ticket>"));
			logger.info("Login wechat success, bot's context is: {}", context);
		} else {
			// should not happen
			throw new WechatException("登录微信出错");
		}
	}

	/**
	 * 模拟初始化微信请求 , 获取登录用户信息，同步请求需要用到的 Key 等
	 * 
	 * <p>对应微信登录成功之后，获取用于初始化整个界面的数据 （好友信息，未读消息，公众号推送消息等等，没细看）</p>
	 * 
	 * @throws WechatException
	 */
	public void initWechat() throws WechatException {
		String url = String.format("%s/webwxinit?r=%s&pass_ticket=%s&skey=%s", context.getBaseURI(),
				DateUtils.getEpochSecondsStr(), context.getPassTicket(), context.getSkey());
		HashMap<String, Object> body = context.getBaseRequestBody();

		RequestBuilder rb = RequestBuilder.post().setUri(url);
		rb.addHeader("Content-Type", ContentType.APPLICATION_JSON.toString());
		rb.setEntity(new StringEntity(JSON.toJSONString(body), ContentType.APPLICATION_JSON));

		String content = this.getContent(rb.build());
		// logger.debug("Initial Wechat response: {}", content);

		if (StringUtils.isNotBlank(content)) {
			JSONObject json = JSON.parseObject(content);
			if (json != null) {
				JSONObject base = json.getJSONObject("BaseResponse");
				// success response
				if (base != null && base.containsKey("Ret") && base.getIntValue("Ret") == 0) {
					this.context.setSelf(json.getJSONObject("User"));
					this.context.setSyncKey(json.getJSONObject("SyncKey"));
					logger.info("Initial wechat success, bot's context is: {}", context);
					return;
				}
			}
		}

		throw new WechatException("微信初始化失败");
	}

	/**
	 *  
	 * 获取当前已添加联系人列表
	 * 
	 * <li>公众号</li>
	 * <li>群</li>
	 * <li>官方账号，比如文件助手，邮箱通知等</li>
	 * <li>当前登录账号（自己）</li>
	 *  
	 * @return
	 * @throws WechatException
	 */
	public List<JSONObject> getContacts() throws WechatException {
		String url = String.format("%s/webwxgetcontact?pass_ticket=%s&skey=%s&r=%s", context.getBaseURI(),
				context.getPassTicket(), context.getSkey(), DateUtils.getEpochSecondsStr());
		RequestBuilder rb = buildPostRequest(url);
		String content = this.getContent(rb.build());
		// logger.debug("Get Wechat contacts response: {}", content);
		if (StringUtils.isNotBlank(content)) {
			JSONObject json = JSON.parseObject(content);
			if (json != null) {
				JSONObject base = json.getJSONObject("BaseResponse");
				// success response
				if (base != null && base.containsKey("Ret") && base.getIntValue("Ret") == 0) {
					JSONArray members = json.getJSONArray("MemberList");
					List<JSONObject> existsContact = new ArrayList<JSONObject>();
					if (null != members) {
						for (int i = 0, len = members.size(); i < len; i++) {
							JSONObject contact = members.getJSONObject(i);
							if ((contact.getIntValue("VerifyFlag") & 8) != 0) {// 公众号/服务号
								continue;
							}
							// 官方联系人
							if (FILTER_USERS.contains(contact.getString("UserName"))) {
								continue;
							}
							// 群聊
							if (contact.getString("UserName").indexOf("@@") != -1) {
								continue;
							}
							// 自己
							if (contact.getString("UserName").equals(context.getSelf().getString("UserName"))) {
								continue;
							}
							existsContact.add(contact);
						}
					}
					return existsContact;
				}
			}
		}

		throw new WechatException("获取微信联系人列表失败");
	}

	/**
	 * 打开状态提醒
	 */
	public void openStatusNotify() throws WechatException {
		logger.info("Try open wechat notify status");

		String url = String.format("%s/webwxstatusnotify?lang=zh_CN&pass_ticket=%s", context.getBaseURI(),
				context.getPassTicket());

		HashMap<String, Object> common = context.getBaseRequestBody();
		HashMap<String, Object> body = Maps.newHashMap(common);
		body.put("Code", 3);
		body.put("FromUserName", context.getSelf().getString("UserName"));
		body.put("ToUserName", context.getSelf().getString("UserName"));
		body.put("ClientMsgId", DateUtils.getEpochSecondsStr());

		RequestBuilder rb = RequestBuilder.post().setUri(url);
		rb.addHeader("Content-Type", ContentType.APPLICATION_JSON.toString());
		rb.setEntity(new StringEntity(JSON.toJSONString(body), ContentType.APPLICATION_JSON));
		String content = this.getContent(rb.build());

		if (StringUtils.isNotBlank(content)) {
			JSONObject json = JSON.parseObject(content);
			if (json != null) {
				JSONObject base = json.getJSONObject("BaseResponse");
				// success response
				if (base != null && base.containsKey("Ret") && base.getIntValue("Ret") == 0) {
					logger.info("Open wechat notify status success");
					return;
				}
			}
		}

		throw new WechatException("开启状态同步失败");
	}

	public String chooseHeartBeatServer() throws WechatException {
		logger.info("Choose wechat heartbeat server");
		for (String server : HEARTBEAT_SERVERS) {
			try {
				String url = "https://" + server + "/cgi-bin/mmwebwx-bin/synccheck";
				HeatbeatResult result = this.heartbeat(url);
				if (result.getCode() == 0) {
					context.setHeartbeatUrl(url);
					logger.info("Choosed wechat heartbeat server -> {}", server);
					return url;
				}
			} catch (Exception e) {
				logger.info("hearbeat" + server + " server is not available", e);
			}
		}
		throw new WechatException("网络可能有异常，无法找到合适的同步服务器");
	}

	/**
	 * 和微信服务器同步，心跳检测
	 */
	private HeatbeatResult heartbeat(String hearbeatUrl) throws WechatException {
		RequestBuilder rb = RequestBuilder.get().setUri(hearbeatUrl);
		rb.addParameter("r", DateUtils.getEpochSecondsStr() + RandomStringUtils.randomNumeric(5));
		rb.addParameter("skey", context.getSkey());
		rb.addParameter("uin", context.getUin());
		rb.addParameter("sid", context.getSid());
		rb.addParameter("deviceid", context.getDeviceId());
		rb.addParameter("synckey", context.getSyncKeyString());
		rb.addParameter("_", String.valueOf(System.currentTimeMillis()));

		String content = this.getContent(rb.build());
		logger.debug("heartbeat response: {}", content);

		HeatbeatResult result = new HeatbeatResult();
		if (StringUtils.isNotBlank(content)) {
			String retcode = PatternUtils.getGroup(content, "retcode:\"(\\d+)\",");
			String selector = PatternUtils.getGroup(content, "selector:\"(\\d+)\"}");
			if (null != retcode && null != selector) {
				result.setCode(Integer.parseInt(retcode));
				result.setSelector(Integer.parseInt(selector));
			}
		}
		return result;
	}

	/**
	 * 有新消息时候进行同步
	 * 
	 * @return
	 * @throws WechatException
	 */
	public JSONObject webWxSync() throws WechatException {
		String url = context.getBaseURI() + "/webwxsync?skey=" + context.getSkey() + "&sid=" + context.getSid()
				+ "&pass_ticket=" + context.getPassTicket();
		HashMap<String, Object> common = context.getBaseRequestBody();
		HashMap<String, Object> body = Maps.newHashMap(common);
		body.put("SyncKey", context.getSyncKey());
		body.put("rr", DateUtils.getEpochSecondsStr());

		RequestBuilder rb = RequestBuilder.post().setUri(url);
		rb.addHeader("Content-Type", ContentType.APPLICATION_JSON.toString());
		rb.setEntity(new StringEntity(JSON.toJSONString(body), ContentType.APPLICATION_JSON));
		String content = this.getContent(rb.build());

		if (StringUtils.isNotBlank(content)) {
			JSONObject json = JSON.parseObject(content);
			if (json != null) {
				JSONObject base = json.getJSONObject("BaseResponse");
				// success response
				if (base != null && base.containsKey("Ret") && base.getIntValue("Ret") == 0) {
					JSONObject newSyncKey = json.getJSONObject("SyncKey");
					context.setSyncKey(newSyncKey);
					return json;
				}
			}
		}
		return null;
	}

	/**
	 * 
	 *  MSGTYPE_TEXT: 1,
		MSGTYPE_IMAGE: 3,
		MSGTYPE_VOICE: 34,
		MSGTYPE_VIDEO: 43,
		MSGTYPE_MICROVIDEO: 62,
		MSGTYPE_EMOTICON: 47,
		MSGTYPE_APP: 49,
		MSGTYPE_VOIPMSG: 50,
		MSGTYPE_VOIPNOTIFY: 52,
		MSGTYPE_VOIPINVITE: 53,
		MSGTYPE_LOCATION: 48,
		MSGTYPE_STATUSNOTIFY: 51,
		MSGTYPE_SYSNOTICE: 9999,
		MSGTYPE_POSSIBLEFRIEND_MSG: 40,
		MSGTYPE_VERIFYMSG: 37,
		MSGTYPE_SHARECARD: 42,
		MSGTYPE_SYS: 10000,
		MSGTYPE_RECALLED: 10002,  // 撤销消息
	 * @throws WechatException
	 */
	public void startHeartbeatThread() throws WechatException {
		this.chooseHeartBeatServer(); // 选择服务器

		logger.info("Start wechat heart beat thread");
		// 初始化线程
		String userName = context.getSelf().getString("UserName");
		ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat(userName + "-%d").build();
		heartBeatPool = Executors.newScheduledThreadPool(1, threadFactory);
		heartBeatPool.scheduleWithFixedDelay(new Runnable() {
			@Override
			public void run() {
				try {
					HeatbeatResult heartbeat = heartbeat(context.getHeartbeatUrl());
					logger.info("{}", heartbeat);

					if (heartbeat.getCode() == 1100) {
						logger.info("你在手机上登出了微信");
					}

					if (heartbeat.getCode() == 0) {
						Integer selector = heartbeat.getSelector();
						if (selector == 2) { // 有新的请求
							JSONObject json = webWxSync();
							JSONArray messages = json.getJSONArray("AddMsgList");
							for (int i = 0, size = messages.size(); i < size; i++) {
								JSONObject message = messages.getJSONObject(i);
								int msgType = message.getIntValue("MsgType");
								if (msgType == 37) { // 新的好友请求
									// 添加新的好友请求到 context中
									context.addNewContactRequest(message);
								}
								logger.info("new message {}", message);
							}
						}
					}
				} catch (Exception e) {

				}
			}
		}, 1, 3, TimeUnit.SECONDS);
	}

	public WechatContext getContext() {
		return context;
	}

	static class HeatbeatResult {
		Integer code = -1;
		Integer selector = -1;

		public Integer getCode() {
			return code;
		}

		public void setCode(Integer code) {
			this.code = code;
		}

		public Integer getSelector() {
			return selector;
		}

		public void setSelector(Integer selector) {
			this.selector = selector;
		}

		@Override
		public String toString() {
			return "HeatbeatResult [code=" + code + ", selector=" + selector + "]";
		}

	}

}
